package com.dman.multi.proto;

import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

import com.dman.multi.proxy.ProxyAdaptor;
import com.dman.multi.proxy.ProxyDelegator;

public class HTTPProtocolHandler implements ProtocolHandler{
	
	private SocketChannel sockChannel = null;
	private Long size = null;
	//private String httpHeaders = "";
	private String [] headerArray = null;
	private boolean isChunkedTransfer = false;
	
	private static final String HTTP_VERSION11 = "HTTP/1.1";
	private static final String HTTP_VERSION10 = "HTTP/1.0";
	
	private static final String HTTP_METHOD = "GET ";
	private static final String HTTP_HOST = "HOST: ";
	private static final String HTTP_RANGE = "RANGE: BYTES=";
	private static final String HTTP_CONN_CLOSE = "CONNECTION: CLOSE";
	private static final String HTTP_LINE_TERMINATOR = "\r\n";
	private static final String HTTP_CHARSET = "US-ASCII";
	private static final String HTTP_CONTENT_LENGTH = "CONTENT-LENGTH:";
	
	private static final String HTTP_PARTIAL_STATUS11 = HTTP_VERSION11 + " 206 ";
	private static final String HTTP_PARTIAL_STATUS10 = HTTP_VERSION10 + " 206 ";
	
	private static final String HTTP_TRANSFER_ENCODING = "TRANSFER-ENCODING:";
	private static final String HTTP_CHUNKED_TRANSFER = "CHUNKED";
	
	//private static final int CHUNK_LENGTH = 10;
	private static final int TERM_LENGTH = HTTP_LINE_TERMINATOR.length();		
	public static final int BUFFER_CAPACITY = 1<<16;
	public static final int CHUNK_LEN_BYTES = 10;
	
	private ByteBuffer dataBuf = null;
	private ByteBuffer lenBuf = ByteBuffer.allocate(CHUNK_LEN_BYTES);
	private ByteBuffer termBuf = ByteBuffer.allocate(TERM_LENGTH);

	@Override
	public Long init(URL url, long offset, ProxyAdaptor proxyAuth) throws IOException,
			UnknownHostException {
		
		if(sockChannel == null){
			
			// implement the proxy
			sockChannel = SocketChannel.open();
			ProxyDelegator pd = ProxyDelegator.getInstance();
			pd.connect(sockChannel, proxyAuth, url);
			
			// send request and read response
			sendRequest(url, offset);
			String resp = recvResponse();
			headerArray = resp.split(HTTP_LINE_TERMINATOR);
			
			// check the status of the response
			String respStatus [] = getStatus();
			if(!respStatus[0].startsWith("2"))
				throw new IOException("Unable to connect to specified server");
			
			// populate the size of the response
			int factor = 1;
			if(getPartialContentStatus().isEmpty())
				factor = -1;
			String strSize = getHeaderValue(HTTP_CONTENT_LENGTH);
			if(!strSize.isEmpty())
				size = factor * Long.parseLong(strSize);
			
			// check if it is a chunked transfer
			String transferEnc = getHeaderValue(HTTP_TRANSFER_ENCODING);
			if(transferEnc.equalsIgnoreCase(HTTP_CHUNKED_TRANSFER)){
				isChunkedTransfer = true;
			}
		}
		
		return size;
	}
	
	public int sendRequest(URL url, long offset) throws IOException{
		int bytesWritten = 0;
		
		// connect to the socket
		String out = HTTP_METHOD + url.toString() + " " + HTTP_VERSION11 + HTTP_LINE_TERMINATOR +
					 HTTP_HOST + url.getHost() + HTTP_LINE_TERMINATOR +
					 HTTP_RANGE + offset + "-" + HTTP_LINE_TERMINATOR + HTTP_CONN_CLOSE + HTTP_LINE_TERMINATOR + HTTP_LINE_TERMINATOR;
		ByteBuffer bb = ByteBuffer.wrap(out.getBytes(HTTP_CHARSET));
		bytesWritten = sockChannel.write(bb);
		
		return bytesWritten;
	}
	
	public String recvResponse() throws IOException{
		
		int byteRead = 0;
		String str = "";
		byte readByte = 0;
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		
		if(sockChannel.isConnected()){
			Socket s = sockChannel.socket();
			InputStream is = s.getInputStream();
			while((byteRead = is.read()) >= 0){
				bos.write(byteRead);
				readByte = (byte) byteRead;
				if(readByte == '\n'){
					str = bos.toString(HTTP_CHARSET);
					if(str.endsWith(HTTP_LINE_TERMINATOR + HTTP_LINE_TERMINATOR))
						break;
				}
			}
		}
		str = str.toUpperCase();
		return str;
	}
	
	public String getPartialContentStatus(){
		String str = getHeaderValue(HTTP_PARTIAL_STATUS11);
		if(str.isEmpty())
			str = getHeaderValue(HTTP_PARTIAL_STATUS10);
		return str;
	}
	
	public String[] getStatus(){
		String str = getHeaderValue(HTTP_VERSION11);
		if(str.isEmpty())
			str = getHeaderValue(HTTP_VERSION10);
		String [] sarr = str.split("\\s");
		return sarr;
	}
	
	public String getHeaderValue(String header){
		//String [] sarr = httpHeaders.split(HTTP_LINE_TERMINATOR);
		String ret = "";
		for(String str: headerArray){
			if(str.startsWith(header)){
				ret = str.substring(header.length()).trim();
				break;
			}
		}
		
		return ret;
	}

	@Override
	public void close() throws IOException {
		if(sockChannel != null)
			sockChannel.close();	
		sockChannel = null;
	}

	@Override
	public SocketChannel socketChannel() throws IOException {
		if(sockChannel == null)
			throw new IOException("Trying to get an un-initialized channel");
		sockChannel.configureBlocking(false);
		return sockChannel;
	}
	
	@Override
	public ByteBuffer read() throws EOFException, IOException {
		if(isChunkedTransfer)
			return readChunks();
		if(dataBuf == null)
			dataBuf = ByteBuffer.allocate(BUFFER_CAPACITY);
		
		dataBuf.clear();
		int num = sockChannel.read(dataBuf);
		dataBuf.flip();
		if(num < 0)
			throw new EOFException();
		return dataBuf;
	}
	
	public ByteBuffer readChunks() throws EOFException, IOException{
		
		// read length of chunk
		if(dataBuf == null || dataBuf.position() == dataBuf.capacity()){
			lenBuf.clear();
			if(sockChannel.read(lenBuf) < 0)
				throw new EOFException("EOF reached");
			while(lenBuf.position() < lenBuf.capacity())
				if(sockChannel.read(lenBuf) < 0)
					throw new EOFException("EOF reached");
			String hexLen = new String(lenBuf.array(), HTTP_CHARSET).trim();
			int chunkLen = Integer.parseInt(hexLen, 16);
			if(chunkLen == 0)
				throw new EOFException("EOF is reached");
			dataBuf = ByteBuffer.allocate(chunkLen);
		}
		
		// read the chunk proper
		dataBuf.mark();
		dataBuf.limit(dataBuf.capacity());
		sockChannel.read(dataBuf);
		dataBuf.limit(dataBuf.position());
		System.setOut(new PrintStream("/tmp/abc.pdf"));
		System.out.write(dataBuf.array());
		System.out.flush();
		if(dataBuf.position() == dataBuf.capacity()){
			termBuf.clear();
			if(sockChannel.read(termBuf) < 0)
				throw new EOFException("EOF reached");
			while(termBuf.position() < termBuf.capacity())
				if(sockChannel.read(termBuf) < 0)
					throw new EOFException("EOF reached");
		}
		dataBuf.reset();
		
		// return data buffer
		return dataBuf;
	}
}
